通过生成的 jsonData,可以拿到页面的基本信息 pageInfo,如:背景色、title等,还有页面布局 layout。主要处理两个问题:

  • 通过 jsonData 数据实现页面渲染;
  • 打包生成 html。

# 打包目录结构

└───build 目录:主要存放 webpack 相关配置
| 	└───static.js // 封装 webpack.run() 方法
| 	└───webpack.base.config.js // webpack 基本配置
|───src
| 	└───App.vue // server 端入口
| 	└───main.js // client 端入口
| 	└──index.tpl.html // 页面模板
| 	└───components.js // 组件引进注册
|───create-html
| 	└───待页面名称 目录
| 		└───config.js // webpack 发布相关配置
| 		└───data.js // jsonData 获取相关逻辑
|───html 目录:打包输出
| 	└───static 目录:静态资源目录
| 		└───...
| 	└──待页面名称.html // 输出的页面
|───index.js // 打包入口文件

# 打包相关

# webpack 公共基本配置 - webpack.base.config.js

用到的 webpack 插件有:vue-loader、optimize-css-assets-webpack-plugin、html-webpack-plugin、html-webpack-inline-source-plugin、ptimize-css-assets-webpack-plugin、babel-loader等。

常规的 webpack 配置跳过,说下 plugins 配置:

 plugins(type) {
     const common = [
         new VueLoaderPlugin(),
         new MiniCssExtractPlugin({
             filename: '[name].[contenthash:6].css',
             chunkFilename: '[id].[contenthash:6].css',
         }),
         new OptimizeCssAssetsWebpackPlugin(),
     ]
     const onDemand = {
         server: [],
         client: [
             new HtmlWebpackPlugin({
                 filename: 'index.template.html',
                 template: path.resolve(__dirname, '../src/index.tpl.html'),
                 inlineSource: '.css$',
                 minify: {
                     collapseWhitespace: true,
                 },
             }),
             new OptimizeCSSAssetsPlugin({}),
             new HtmlWebpackInlineSourcePlugin(),
         ],
     }
     return [].concat(common).concat(onDemand[type] || [])
 },

这通过传入 type 类型判断,判断是 client 端还是 server 端。公共需要的是 vue-loader,但 client 需要的是 html-webpack-plugin。

# client webpack 配置

除了 webpack.base.config.js 公共配置,client 的 webpack 需要配置的有:entry、output、端判断变量等。

const clientWebpackConfig = {
    ...webpackBaseConfig,
    mode,
    entry: path.resolve(__dirname, './src/main.js'),
    output: {
        path: path.resolve(__dirname, '.', publishConfig.outputPath),
        filename: '[name].[contenthash:8].js',
        publicPath: publishConfig.publicPath,
    },
    plugins: [
        ...webpackBaseConfig.plugins('client'),
        new webpack.DefinePlugin({
            'process.env': JSON.stringify({
                IS_SERVER: false,
            }),
        }),
    ],
}

# server webpack 配置

除了 webpack.base.config.js 公共配置,server 的 webpack 需要配置的有:entry、output、端判断变量等。

const serverWebpackConfig = {
    ...webpackBaseConfig,
    mode,
    entry: {
        app: path.resolve(__dirname, './src/App.vue'),
    },
    output: {
        path: path.resolve(__dirname, '.', publishConfig.outputPath),
        libraryTarget: 'commonjs',
        publicPath: publishConfig.publicPath,
    },
    plugins: [
        ...webpackBaseConfig.plugins('server'),
        new webpack.DefinePlugin({
            'process.env': JSON.stringify({
                IS_SERVER: true,
            }),
        }),
    ],
}

# static.js - webpack.run 封装

这里封装要给 Static 类,输入 webpack 配置,输出打包好的 bundle。

const webpack = require('webpack')

class Static {
  constructor(options) {
    this.options = options
  }

  run() {
    const webpacks = []
    Object.keys(this.options).forEach((k) => {
      webpacks.push(webpack(this.options[k]))
    })
    return Promise.all(
      webpacks.map(
        (web) =>
          new Promise((reslove) => {
            web.run((err, stats) => {
              reslove()
            })
          })
      )
    )
  }
}

module.exports = {
  Static,
}

# server 入口 - 渲染生成页面

通过封装一个入口组件 App.vue,将 jsonData 通过 props 传入,然后通过 js 引进相关的渲染组件,最后通过 v-for 配合动态组件<component :is="" />可以实现 组件渲染。

<template>
  <div class="h5-page" :style="pageStyle">
    <div class="layout-item" v-for="(item, index) in layout" :key="index">
      <component :is="item.component" :dynamicStyle="item.config"></component>
    </div>
  </div>
</template>

<script>
import components from "./components.js";
export default {
  name: "H5Page",
  components: components,
  props: {
    pageConfig: {
      type: Object,
      default: () => [],
    },
  },
  data() {
    return {
      layout: [],
    };
  },
  computed: {
    pageStyle() {
      return {
        backgroundColor:
          this.$props.pageConfig.pageInfo.backgroundColor || "#fff",
      };
    },
  },
  created() {
    this.bindComponent();
  },
  methods: {
    /**
     * 给每个布局绑定唯一组件
     */
    bindComponent() {
      if (!Object.keys(components).length) {
        return;
      }
      // 给 layout 绑定对应组件
      const layout = this.$props.pageConfig.layout || [];
      if (!layout.length) {
        return;
      }
      layout.forEach((item) => {
        item.component = components[item.type];
      });
      this.layout = layout;
    },
  },
};
</script>

<style lang="scss">
.h5-page {
  width: 100vw;
}
</style>

这里将组件注入单独抽离,后期有新的组件使用,改动 components.js 即可。

/************************ 组件注册 ************************/

if (!process.env.IS_SERVER) {
  // 解决:UnhandledPromiseRejectionWarning: ReferenceError: window is not defined 问题
  const H5Editor = require('h5-editor')

  module.exports = {
    EdText: H5Editor.default.EdText,
    EdImage: H5Editor.default.EdImage
  }
}

注意:上面有一个全局变量(通过 webpack.DefinePlugin 插件注入变量)判断是服务端还是客户端的判断,因为打包使用的是vue-server-renderer (opens new window)相关逻辑。故:存在两个端,client 和 server,server 端没有 window、document 等对象。

# client 入口 - 注入全局数据 window.__initData__

主要做两件事:

  • 挂载 Vue 实例到目标节点;
  • 注入全局 window.__ininData__
import Vue from 'vue'
import App from './App.vue'

let props = {}
if (window.__initData__) {
  props = { pageConfig: window.__initData__.pageConfig }
}

new Vue({
  components: { App: App },
  render: (h) => h('App', { props }),
}).$mount('#app')

# 页面 html 模板

<!DOCTYPE html>
<html style="font-size:100px;">

<head>
  <title>{{ title }}</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport"
    content="width=device-width,initial-scale=1.0,initial-scale=1.0,maximum-scale=1.0,user-scalable=no,minimal-ui">
  <meta name="description" content="{{description}}" />
</head>

<body style="font-size: 16px">
  <div id="app">
    <!--vue-ssr-outlet-->
  </div>
  <script>
    {{{ script }}}
  </script>
</body>

</html>

# index.js - 构建

打包使用的是vue-server-renderer (opens new window)相关逻辑。

主要做几件事:

  • 通过 webpack 打包输出 templateHtml 和 bundle.js(还没有注入页面数据 window.__initData__)
 const compiler = new Static({
    app: appWebpackConfig,
    client: clientWebpackConfig,
  })
  await compiler.run()
  • 通过 vue-server-renderer结合 templateHtml 和 bundle.js 生成 renderer 对象
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./html/static/index.template.html', 'utf-8'),
})
const main = require(path.resolve(
    __dirname,
    `./${publishConfig.outputPath}/app.js`
))
  • 通过 /create-html/待打包页面名/data.js脚本获取 jsonData 数据

  • 将 jsonData 相关数据通过 context,注入到模板页面中

const pageConfigData = await dataHandler()
const { title = '', description = '' } = pageConfigData.pageInfo
const context = {
    title,
    description,
    script: `window.__initData__=${JSON.stringify({
        pageConfig: pageConfigData,
    })}`,
}
const app = new Vue({
    data: {},
    components: { App: main.default },
    render: (h) => h('App', { props: { pageConfig: pageConfigData } }),
})
  • 通过 renderer 对象输出 html 并写入到目标目录下。
renderer.renderToString(app, context, (err, html) => {
    if (!html) {
        return
    }
    const filePath = path.resolve(__dirname, `${publishConfig.filePath}`)
    fse.ensureDirSync(filePath)
    // name 为 nodejs 命令行参数:待打包页面名称
    fs.writeFile(`${filePath}/${name}.html`, html, function(err) {
        if (err) {
            console.error(`>>>> 生成 ${name} 页面失败!`, err)
            return
        }
        console.log(
            `${publishConfig.filePath}/${name}.html` + ':数据写入成功!'
        )
    })
})

over~